{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "5c754bef",
   "metadata": {},
   "source": [
    "## 图像预处理\n",
    "\n",
    "简单的灰度图形态学膨胀加粗\n",
    "- MORPH_RECT：方形膨胀，适合文字\n",
    "- kernel_size=3 : 膨胀核大小，3x3已经足够大多数情况\n",
    "- iterations=2 : 膨胀2次，适合文字加粗"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 63,
   "id": "1dd5ae22",
   "metadata": {},
   "outputs": [],
   "source": [
    "def _dilate_image_fixed(image_path, kernel_size=3, iterations=1):\n",
    "    \"\"\"修复后的形态学膨胀，确保文字为黑色\"\"\"\n",
    "    import os\n",
    "    \n",
    "    # 读取并转为灰度图\n",
    "    img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)\n",
    "    \n",
    "    # 检查图片是否为白底黑字，如果是黑底白字则反转\n",
    "    mean_val = np.mean(img)\n",
    "    if mean_val < 127:  # 如果平均值偏暗，说明是黑底白字，需要反转\n",
    "        img = 255 - img\n",
    "    \n",
    "    # 二值化处理，确保文字为黑色(0)，背景为白色(255)\n",
    "    _, binary = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)\n",
    "    \n",
    "    # 如果文字是白色的，反转图像\n",
    "    if np.sum(binary == 255) > np.sum(binary == 0):  # 白色像素多于黑色\n",
    "        binary = 255 - binary\n",
    "    \n",
    "    # 创建膨胀核\n",
    "    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_size, kernel_size))\n",
    "    \n",
    "    # 执行膨胀（膨胀黑色文字）\n",
    "    dilated = cv2.dilate(binary, kernel, iterations=iterations)\n",
    "    \n",
    "    # 保存结果\n",
    "    base_name = os.path.splitext(image_path)[0]\n",
    "    ext = os.path.splitext(image_path)[1]\n",
    "    new_path = f\"{base_name}_dilated_fixed{ext}\"\n",
    "    cv2.imwrite(new_path, dilated)\n",
    "    \n",
    "    return new_path\n",
    "\n",
    "# 使用修复版本\n",
    "dilated_image = _dilate_image_fixed(original_image, kernel_size=3, iterations=1)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "59d54b77",
   "metadata": {},
   "source": [
    "## VL调用"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 111,
   "id": "8009af3f",
   "metadata": {},
   "outputs": [],
   "source": [
    "import dashscope\n",
    "\n",
    "prompt = \"找到“十一、公司业务主管领导”下方的可签字区域，以 JSON 格式输出其bbox 的坐标，不要输出`json'代码段。\"\n",
    "# prompt =\"\"\"\n",
    "# ## 任务1\n",
    "# 从图片中提取“八、二级单位业务主管部门审查意见”区域中“业务主管部门签字 (生产、管道或工程)”下方的三个关键手写内容：**位于顶部的批示文本**、**签字**和**日期**。请以JSON格式输出，不要输出 ```json``` 代码段。\n",
    "\n",
    "# ## 任务2\n",
    "# 定位任务中提取的三个元素的位置，以 JSON 格式输出所有的bbox 的坐标，不要输出`json'代码段。\n",
    "\n",
    "# ## 任务3\n",
    "# 请描述你在任务1中要提取的内容范围，你是如何理解的，你会如何找到要提取的内容。\n",
    "\n",
    "# #### 找到相关内容的方法:\n",
    "# - 定位至表头为 \"**八、二级单位业务主管部门审查意见**\" 的那一栏。\n",
    "# - 定位到表头为 \"业务主管部门签字（生产、管道或工程）\" 子栏目。\n",
    "# - 查看其下方对应的具体填写区域，在其中识别出以下三个关键手写内容的位置并逐一处理：\n",
    "#   1. **位于顶部的批示文本：** 寻找位于\"(生产、管道或工程):\" 下方空白区域中**手写中文内容**。如果该区域没有任何手写内容，则返回空字符串。\n",
    "#   2. **签字：** 根据视觉线索定位到“签字:”字样，并在其**紧邻的右侧**获取对应的**手写签名**。\n",
    "#   3. **日期：** 寻找靠近该区域末尾、通常位于“年 月 日”字样附近或其下方，具有日期格式的**手写数字和文字组合**（例如“2025 年 6 月 7 日”）。\n",
    "# \"\"\"\n",
    "\n",
    "file_path = \"file://C:/Users/serap/Pictures/1.jpg\"\n",
    "#file_path = \"http://bailian-datahub-data-share-prod.oss-cn-beijing.aliyuncs.com/runtime/temp/1534029851508492/12128722/134d59c09eff41ae89ab3717d0ca4bb8.1753775086497.jpg?Expires=1754034286&OSSAccessKeyId=LTAI5tKzNnKPFwCJSCpxx51h&Signature=IkE5YHv%2B4QUrIvPArsp7ptDN2hE%3D\"\n",
    "\n",
    "\n",
    "messages = [\n",
    "{\n",
    "    \"role\": \"system\",\n",
    "    \"content\": [\n",
    "    {\"text\": \"You are a helpful assistant.\"}]\n",
    "},\n",
    "{\n",
    "    \"role\": \"user\",\n",
    "    \"content\": [\n",
    "    #{\"image\": \"file://C:/Users/serap/Pictures/1.png\"},\n",
    "    {\"image\": file_path},\n",
    "    {\"text\": prompt}]\n",
    "}]\n",
    "\n",
    "response = dashscope.MultiModalConversation.call(\n",
    "    #若没有配置环境变量， 请用百炼API Key将下行替换为： api_key =\"sk-xxx\"\n",
    "    #api_key = os.getenv('DASHSCOPE_API_KEY'),\n",
    "    api_key=\"sk-0e687ddcf0164a6fb66c1096447223c4\",  # 阿里百炼大模型API获取：https://bailian.console.aliyun.com/?apiKey=1#/api-key\n",
    "    model = 'qwen-vl-max-latest',  # 此处以qwen2.5-vl-32b-instruct为例，可按需更换模型名称。模型列表：https://help.aliyun.com/zh/model-studio/models\n",
    "    vl_high_resolution_images=False,\n",
    "    messages = messages\n",
    ")\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "de76c29d",
   "metadata": {},
   "source": [
    "## 显示输出"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e43549ae",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "原始图像中的bbox坐标：(582, 1169, 656, 1196)\n",
      "标记完成，保存为 1_marked_result.png\n"
     ]
    }
   ],
   "source": [
    "import math\n",
    "from PIL import Image, ImageDraw\n",
    "\n",
    "\n",
    "def get_original_coords(\n",
    "    original_image_path, \n",
    "    scaled_bbox,  # 模型返回的基于缩放后图像的bbox：(x1, y1, x2, y2)\n",
    "    vl_high_resolution_images=False  # 是否启用高分辨率模式\n",
    "):\n",
    "    \"\"\"\n",
    "    将模型返回的缩放后图像的bbox坐标映射到原始图像坐标\n",
    "    \n",
    "    参数：\n",
    "        original_image_path: 原始图像的本地路径\n",
    "        scaled_bbox: 模型返回的bbox，格式为(x1, y1, x2, y2)\n",
    "        vl_high_resolution_images: 是否启用高分辨率模式（影响缩放上限）\n",
    "    \n",
    "    返回：\n",
    "        原始图像中的bbox坐标，格式为(x1, y1, x2, y2)\n",
    "    \"\"\"\n",
    "    # 1. 加载原始图像，获取原始尺寸\n",
    "    with Image.open(original_image_path) as img:\n",
    "        original_height = img.height\n",
    "        original_width = img.width\n",
    "    \n",
    "    # 2. 计算模型对图像的缩放参数（参考文档中图像转Token的缩放逻辑）\n",
    "    # 2.1 确定缩放后的图像尺寸（与模型内部处理逻辑一致）\n",
    "    h_bar, w_bar = calculate_scaled_size(\n",
    "        original_height, original_width, vl_high_resolution_images\n",
    "    )\n",
    "    \n",
    "    # 2.2 计算原始图像到缩放图像的缩放比例\n",
    "    scale_h = h_bar / original_height  # 高度缩放比例（缩放后/原始）\n",
    "    scale_w = w_bar / original_width   # 宽度缩放比例（缩放后/原始）\n",
    "    \n",
    "    # 3. 将缩放后的bbox坐标反向映射到原始图像\n",
    "    x1_scaled, y1_scaled, x2_scaled, y2_scaled = scaled_bbox\n",
    "    x1_original = int(round(x1_scaled / scale_w))\n",
    "    y1_original = int(round(y1_scaled / scale_h))\n",
    "    x2_original = int(round(x2_scaled / scale_w))\n",
    "    y2_original = int(round(y2_scaled / scale_h))\n",
    "    \n",
    "    # 4. 确保坐标在原始图像范围内（防止越界）\n",
    "    x1_original = max(0, min(x1_original, original_width))\n",
    "    y1_original = max(0, min(y1_original, original_height))\n",
    "    x2_original = max(0, min(x2_original, original_width))\n",
    "    y2_original = max(0, min(y2_original, original_height))\n",
    "    \n",
    "    return (x1_original, y1_original, x2_original, y2_original)\n",
    "\n",
    "\n",
    "def calculate_scaled_size(original_height, original_width, vl_high_resolution_images):\n",
    "    \"\"\"计算模型内部对图像的缩放尺寸（复刻文档中缩放逻辑）\"\"\"\n",
    "    # 基础参数（文档定义：每28x28像素对应1个Token）\n",
    "    factor = 28\n",
    "    min_pixels = factor * factor * 4  # 最小4个Token对应的像素数\n",
    "    if vl_high_resolution_images:\n",
    "        max_pixels = 16384 * factor * factor  # 高分辨率模式：上限16384 Token\n",
    "    else:\n",
    "        max_pixels = 1280 * factor * factor   # 默认模式：上限1280 Token\n",
    "    \n",
    "    # 初始调整为28的整数倍\n",
    "    h_bar = round(original_height / factor) * factor\n",
    "    w_bar = round(original_width / factor) * factor\n",
    "    current_pixels = h_bar * w_bar\n",
    "    \n",
    "    # 若超过最大像素限制，缩小图像\n",
    "    if current_pixels > max_pixels:\n",
    "        beta = math.sqrt((original_height * original_width) / max_pixels)\n",
    "        h_bar = math.floor(original_height / beta / factor) * factor\n",
    "        w_bar = math.floor(original_width / beta / factor) * factor\n",
    "    # 若低于最小像素限制，放大图像\n",
    "    elif current_pixels < min_pixels:\n",
    "        beta = math.sqrt(min_pixels / (original_height * original_width))\n",
    "        h_bar = math.ceil(original_height * beta / factor) * factor\n",
    "        w_bar = math.ceil(original_width * beta / factor) * factor\n",
    "    \n",
    "    return h_bar, w_bar\n",
    "\n",
    "\n",
    "# ----------------------示例用法----------------------\n",
    "if __name__ == \"__main__\":\n",
    "    # 1. 模型返回的基于缩放后图像的bbox（示例）\n",
    "    scaled_bbox = (453, 896, 510, 917)  # 假设模型返回的坐标位置\n",
    "    \n",
    "    # 2. 映射到原始图像坐标\n",
    "    raw_path = file_path.replace(\"file://\", \"\") if file_path.startswith(\"file://\") else file_path\n",
    "    original_image_path = raw_path  # 你的原始图像路径\n",
    "    original_bbox = get_original_coords(\n",
    "        original_image_path,\n",
    "        scaled_bbox,\n",
    "        vl_high_resolution_images=False  # 与调用模型时的参数保持一致\n",
    "    )\n",
    "    \n",
    "    # 3. 输出结果\n",
    "    print(f\"原始图像中的bbox坐标：{original_bbox}\")\n",
    "    \n",
    "    # 4. （可选）使用Pillow截取原始图像中的区域\n",
    "    # with Image.open(original_image_path) as img:\n",
    "    #     cropped_img = img.crop(original_bbox)\n",
    "    #     cropped_img.save(\"1_cropped_result.png\")\n",
    "    #     print(\"截取完成，保存为 cropped_result.png\")\n",
    "\n",
    "    # 在原图上画蓝色框\n",
    "    with Image.open(original_image_path) as img:\n",
    "        # 创建可绘制对象\n",
    "        draw = ImageDraw.Draw(img)\n",
    "        \n",
    "        # 画蓝色矩形框（线宽为3像素）\n",
    "        x1, y1, x2, y2 = original_bbox\n",
    "        draw.rectangle([x1, y1, x2, y2], outline='blue', width=3)\n",
    "        \n",
    "        # 保存带框的图像\n",
    "        img.save(\"1_marked_result.png\")\n",
    "        print(\"标记完成，保存为 1_marked_result.png\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d1ea0237",
   "metadata": {},
   "source": [
    "## 计算图片需要消耗多少token\n",
    "\n",
    "每28x28像素对应一个Token，一张图最少需要4个Token。您可以通过以下代码估算图像的Token："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "910092e0",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "缩放前的图像尺寸为：高度为1384，宽度为982\n",
      "缩放后的图像尺寸为：高度为1176，宽度为840\n",
      "图像的Token数为1262\n"
     ]
    }
   ],
   "source": [
    "import math\n",
    "# 使用以下命令安装Pillow库：pip install Pillow\n",
    "from PIL import Image\n",
    "\n",
    "def token_calculate(image_path):\n",
    "    # 打开指定的PNG图片文件\n",
    "    image = Image.open(image_path)\n",
    "\n",
    "    # 获取图片的原始尺寸\n",
    "    height = image.height\n",
    "    width = image.width\n",
    "    print(f\"缩放前的图像尺寸为：高度为{height}，宽度为{width}\")\n",
    "    # 将高度调整为28的整数倍\n",
    "    h_bar = round(height / 28) * 28\n",
    "    # 将宽度调整为28的整数倍\n",
    "    w_bar = round(width / 28) * 28\n",
    "    \n",
    "    # 图像的Token下限：4个Token\n",
    "    min_pixels = 28 * 28 * 4\n",
    "    # 图像的Token上限：1280个Token\n",
    "    max_pixels = 1280 * 28 * 28\n",
    "        \n",
    "    # 对图像进行缩放处理，调整像素的总数在范围[min_pixels,max_pixels]内\n",
    "    if h_bar * w_bar > max_pixels:\n",
    "        # 计算缩放因子beta，使得缩放后的图像总像素数不超过max_pixels\n",
    "        beta = math.sqrt((height * width) / max_pixels)\n",
    "        # 重新计算调整后的高度，确保为28的整数倍\n",
    "        h_bar = math.floor(height / beta / 28) * 28\n",
    "        # 重新计算调整后的宽度，确保为28的整数倍\n",
    "        w_bar = math.floor(width / beta / 28) * 28\n",
    "    elif h_bar * w_bar < min_pixels:\n",
    "        # 计算缩放因子beta，使得缩放后的图像总像素数不低于min_pixels\n",
    "        beta = math.sqrt(min_pixels / (height * width))\n",
    "        # 重新计算调整后的高度，确保为28的整数倍\n",
    "        h_bar = math.ceil(height * beta / 28) * 28\n",
    "        # 重新计算调整后的宽度，确保为28的整数倍\n",
    "        w_bar = math.ceil(width * beta / 28) * 28\n",
    "    return h_bar, w_bar\n",
    "\n",
    "# 将test.png替换为本地的图像路径\n",
    "h_bar, w_bar = token_calculate(\"C:/Users/serap/Pictures/1.png\")\n",
    "print(f\"缩放后的图像尺寸为：高度为{h_bar}，宽度为{w_bar}\")\n",
    "\n",
    "# 计算图像的Token数：总像素除以28 * 28\n",
    "token = int((h_bar * w_bar) / (28 * 28))\n",
    "\n",
    "# 系统会自动添加<|vision_bos|>和<|vision_eos|>视觉标记（各计1个Token）\n",
    "print(f\"图像的Token数为{token + 2}\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "base",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.19"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
